# ruby_fetion/fetion.rb
# -*- encoding: utf-8 -*-
# License: GPL v2
# Author:  jhsea3do@gmail.com
# Version: 0.1

class Fetion
  def login; end
  def logout; end
  def send_sms(sms_text, mobileno); end
  def get_contact_list; end
end

class FetionUri
  EXPR_SIP = /sip:(.*?)@(.*?);p=(.*)$/
  EXPR_TEL = /tel:(.*)$/
end

class FetionBuddy
  attr_accessor :uri, :user_id, :fetionno, :mobileno
  attr_accessor :local_name, :relation_status, :feike_read_version, :online_notify
  attr_accessor :list_name, :list_id
end

class FetionClient < Fetion

  FETION_DOMAIN      = "fetion.com.cn"
  FETION_SYSTEM_CONFIG_URI\
                     = "http://nav.fetion.com.cn/nav/getsystemconfig.aspx"

  FETION_SIPC_PROXY  = '221.130.44.193:8080'
  FETION_SSI_URI     = 'https://uid.fetion.com.cn/ssiportal/SSIAppSignIn.aspx'
  FETION_HTTP_TUNNEL = 'HTTP://221.130.44.193/ht/sd.aspx'

  FETION_MOBILE_EXPR = /^13[4-9]\d{8}$|^15[012789]\d{8}$|^188\d{8}$|^147\d{8}$/

  attr_reader :mobileno, :fetionno, :password
  attr_reader :cookie, :ssic, :nonce, :cnonce, :fetion_uri
  attr_reader :sipc, :http_tunnel, :logger
  attr_reader :seq, :aseq, :iseq, :mseq, :rseq, :sseq, :subseq, :qseq
  attr_reader :buddies, :contacts

  def initialize options = {}
    @mobileno   = options[:mobileno]
    @fetionid   = options[:fetionid]
    @password   = options[:password]
    @logger     = options[:logger] || Logger.new(STDOUT)
    @domain     = FETION_DOMAIN
    @buddies    = false # TODO
    @contacts   = false
    @cookie     = false
    @ssic       = false
    @seq        = 0
    @nonce      = false
    @cnonce     = false
    @fetion_uri = false
    @sipc       = false
    @fp         = FetionProtocol
    @logined    = false
  end

  def init force = false
    return if @inited && !force
    begin
      init_system_config true
      @seq = next_seq
      @aseq,@iseq,@mseq,@rseq,@sseq,@subseq,@qseq = 0,0,0,0,0,0,0
      @inited = true
    rescue
      @logger.fatal $!
      raise $!
    end
  end

  def login
    init true
    @seq = next_seq    
    cmds = @fp.LOGIN_CMDS
    data = compress(@fp.LOGIN % 400)
    req0 = (cmds[0] % [@domain, @fetionno, @seq, next_rseq, data.length]) + data
    res0 = @sipc.request req0
    @nonce = res0.scan(/nonce="(.*)"/)[0][0]
    @logger.debug "# nonce = #{@nonce}"
    req1 = (cmds[1] % [@domain, @fetionno, @seq, next_rseq, gen_digest, @cnonce, data.length]) + data
    res1 = @sipc.request req1
    raise "error: login failed" if res1.scan(@fp.LOGIN_SUCC).empty?
    @logined = true
  end

  def login?
    @logined
  end

  def logout
    cmds = @fp.LOGOUT_CMDS
    req0 = cmds[0] % [@domain, @fetionno, next_seq, next_rseq]
    res0 = @sipc.request req0
    # raise "error: logout failed" if res0.scan(@fp.LOGOUT_SUCC).empty?
    logger.error "! logout failed" if res0.scan(@fp.LOGOUT_SUCC).empty?
  end

  def keep_alive
    cmds = @fp.KEEP_ALIVE_CMDS
    req0 = cmds[0] % [@domain, @fetionno, next_seq, next_rseq]
    res0 = @sipc.request req0
  end

  def invite fetionno
    add_buddy fetionno
  end

  def follow; end
  def leave;   end

  def read_notify
    notify = @sipc.queue.shift
    if !notify.nil?
      m0     = notify.match(/^(M|BN)\ (\d+)\ SIP.*/)
      if !m0.nil?
        type   = m0[1]
        to     = m0[2]
        if "BN" == type # event
          logger.info("bn: #{notify}")
#BN 485474907 SIP-C/2.0
#N: permission
#I: 2
#Q: 1 BN
#L: 204

#<events><event type="BuddyPermissionChanged"><permissions version="322671746"><permission user-id="228966961" uri="sip:669635763@fetion.com.cn;p=4068" values="identity=0;"/></permissions></event></events>

        elsif "M" == type
          mime   = notify.match(/C: (.*?)\r\n/)[1]  if notify.match(/C: (.*?)\r\n/)
          xi     = notify.match(/XI: (.*?)\r\n/)[1] if notify.match(/XI: (.*?)\r\n/)
          time   = notify.match(/D: (.*?)\r\n/)[1]  if notify.match(/D: (.*?)\r\n/)
          from   = notify.match(/F: (.*?)\r\n/)[1]  if notify.match(/F: (.*?)\r\n/)
          k      = notify.match(/K: (.*?)\r\n/)[1]  if notify.match(/K: (.*?)\r\n/)
          size   = notify.match(/L: (.*?)\r\n/)[1].to_i  if notify.match(/L: (\d+)/)
          text   = notify[notify.size - size, size]
          logger.info("from: #{from}, text: #{text}, xi: #{xi}")
        end
      end
    end
  end

  def get_contact_list
    @buddies = []
    cmds = @fp.GET_CONTACT_LIST_CMDS
    data = compress(@fp.GET_CONTACT_LIST)
    req0 = cmds[0] % [@domain, @fetionno, next_seq, next_sseq, data.length] + data
    res0 = @sipc.request req0
    res0.scan(%r{<results>.*?</results>}).each do |results|
      doc = REXML::Document.new(results)
      doc.elements.each("results/contacts/allow-list/contact") do |contact|
        @buddies << {:uri => contact.attributes["uri"]}
      end
    end
    # @logger.debug "buddies: #{@buddies.inspect}"
    @buddies
  end

  def get_contacts_info
    @contacts = []
    cmds = [%{S %s SIP-C/2.0\r\nF: %s\r\nN: GetContactsInfo\r\nI: %s\r\nQ: %s S\r\n\L: %s\r\n\r\n}]
    data = %{<args>
      <contacts attributes="provisioning;impresa;mobile-no;nickname;name;gender;portrait-crc;ivr-enabled" extended-attributes="score-level">
      %s</contacts></args>}
    buds = ""
    @buddies.each do |buddy|
      buds += "<contact uri=\"#{buddy[:uri]}\" />"
    end
    data = compress(data % [buds])
    req0 = cmds[0] % [@domain, @fetionno, next_seq, next_sseq, data.length] + data
    res0 = @sipc.request req0
    raise "error: get_contacts_info failed" if res0.scan('SIP-C/2.0 200 OK').empty?
    res0.scan(%r{<results>.*?</results>}).each do |results|
      doc = REXML::Document.new(results)
      doc.elements.each("results/contacts/contact") do |contact|
        attrs = contact.children.size == 0 ? {} : contact.children.first.attributes
        @contacts << [contact.attributes["uri"], attrs] # Contact.new(contact.attributes["uri"], attrs)
      end
    end
    # @logger.debug @contacts.inspect
    @contacts
  end

  def get_contacts expr = /.*/
    @contacts.select{|uri, attrs| "#{attrs}" =~ expr}
  end

  def add_buddy fetionno, desc = nil, phrase_id = 0
    cmds = [%{S %s SIP-C/2.0\r\nF: %s\r\nN: AddBuddy\r\nI: %s\r\nQ: %s S\r\n\L: %s\r\n\r\n}]
    data = %{<args><contacts><buddies><buddy uri="%s" desc="%s" addbuddy-phrase-id="%s" /></buddies></contacts></args>}
    uri  = fetionno.match(FETION_MOBILE_EXPR).nil? ? "sip:#{fetionno}" : "tel:#{fetionno}"
    desc = Iconv.iconv('utf-8', 'gb18030', URI.decode('%D2%C6%B6%AF%D0%A3%D4%B0')).to_s if desc.nil?
    data = compress(data % [uri, desc, phrase_id])
    req0 = cmds[0] % [@domain, @fetionno, next_seq, next_sseq, data.length] + data
    res0 = @sipc.request req0
    raise "error: add_buddy failed" if res0.scan('SIP-C/2.0 200 OK').empty?
    # SIP-C/2.0 522 No Subscription

    # SIP-C/2.0 200 OK
    # to
    # BN 669635763 SIP-C/2.0
    # N: contact
    # I: 2
    # Q: 2 BN
    # L: 199
    #
    # <events><event type="UpdateBuddy"><contacts version="322589902"><buddies><buddy user-id="448863234" uri="sip:434612445@fetion.com.cn;p=829" relation-status="1"/></buddies></contacts></event></events>

# from
# I: 3
# Q: 1 S
# L: 239

# <results><contacts version="322671746"><buddies><buddy uri="sip:669635763@fetion.com.cn;p=4068" local-name="" buddy-lists="" online-notify="0" desc="?§???¨?????-" relation-status="0" addbuddy-phrase-id="0" /></buddies></contacts></results>
  end

  def add_buddies fetionnos, desc = nil, phrase_id = 0
    fetionnos.each do |fetionno|
      add_buddy fetionno, desc, phrase_id
      sleep 1 # TODO
    end
  end

  def send_sms sms_text, to = false
    data = Iconv.iconv('UTF-8', 'UTF-8', sms_text)[0]
    cmds = @fp.SEND_SMS_CMDS
    uri  = nil
    # cmd  = to == false ? cmds[1] : cmds[0]
    if !to 
      cmd = cmds[1]
      uri = @fetion_uri # tel:#{mobileno} 
    else
      cmd = cmds[0] 
      if to.match(FETION_MOBILE_EXPR) 
        contacts = get_contacts(%r{#{to}})
        uri = contacts[0][0] if contacts.size > 0  
      else
        # TODO
      end
    end
    if !uri.nil?
      req0 = cmd % [@domain, @fetionno, next_seq, next_mseq, uri, data.length] + data
      res0 = @sipc.request req0
      raise "error: send_sms failed" if res0.scan(@fp.SEND_SMS_SUCC).empty?
    else
      raise "error: send_sms failed, dest mobile not in your contacts!" # TODO
    end
  end

  def get_offline_messages
    cmds = @fp.GET_OFFLINE_MESSAGES_CMDS
    req0 = cmds[0] % [@domain, @fetionno, next_seq, next_sseq]
    res0 = @sipc.request req0
  end

  def gen_digest
    @cnonce = Digest::MD5.hexdigest(rand.to_s)
    @logger.debug "# cnonce = #{@cnonce}"
    salt = Digest::MD5.digest "#{@fetionno}:#{@domain}:#{@password}"
    h1   = Digest::MD5.hexdigest("#{salt}:#{@nonce}:#{@cnonce}").upcase
    h2   = Digest::MD5.hexdigest("REGISTER:#{@fetionno}").upcase
    Digest::MD5.hexdigest("#{h1}:#{@nonce}:#{h2}").upcase
  end

  def get_system_config config_uri = FETION_SYSTEM_CONFIG_URI
    @logger.debug "< #{config_uri}"
    uri  = URI.parse config_uri
    http = Net::HTTP.new uri.host, uri.port
    data = FetionProtocol.GET_SYSTEM_CONFIG % @mobileno
    resp = http.post uri.path, data, {'Content-Type' => 'application/x-www-form-urlencoded'}
    @logger.debug "> #{resp.body}"
    resp.body
  end

  def ssi_app_signin ssi_uri = FETION_SSI_URI
    @logger.debug "< #{ssi_uri}"
    uri  = URI.parse ssi_uri
    path = "#{uri.path}?mobileno=#{@mobileno}&pwd=#{@password}"
    http = Net::HTTP.new uri.host, uri.port
    http.use_ssl     = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE # turn off SSL warnning
    resp = http.get path, nil
    @logger.debug "> #{resp.body}"
    raise "error: ssi_app_signin failed" if resp.body.scan('status-code="200"').empty?
    # 200: '<?xml version="1.0" encoding="utf-8" ?><results status-code="200"><user uri="sip:669635763@fetion.com.cn;p=4068" mobile-no="13816744731" user-status="101" user-id="228966961"><credentials></credentials></user></results>'
    # 401: '<?xml version="1.0" encoding="utf-8" ?><results status-code="401" />'
    @cookie = resp.response['set-cookie'].split(';')[0]
    @logger.debug "> #{@cookie}"
    @ssic   = @cookie.split('=')[1]
    resp.body
  end

  def get_fetion_uri xml
    uri = nil
    doc = REXML::Document.new xml
    doc.elements.each('//results/user'){|elem| uri = elem.attribute("uri").value}
    uri
  end

  def init_system_config lazy = false
    sipc_proxy   = FETION_SIPC_PROXY
    ssi_uri      = FETION_SSI_URI
    http_tunnel  = FETION_HTTP_TUNNEL
    if !lazy
      config_doc = REXML::Document.new get_system_config
      elems      = config_doc.elements
      elems.each('//sipc-proxy')     {|elem| sipc_proxy = elem.text}
      elems.each('//ssi-app-sign-in'){|elem| ssi_uri = elem.text}
      elems.each('//http-tunnel')    {|elem| http_tunnel = elem.text}
    end
    @sipc        = SIPC.new :uri => "sipc://#{sipc_proxy}", :logger => @logger
    @fetion_uri  = get_fetion_uri(ssi_app_signin(ssi_uri))
    @http_tunnel = http_tunnel
    @fetionno    = @fetion_uri.scan(/sip:([0-9]+)@/)[0][0]
    @logger.debug "# sipc = #{@sipc}"
    @logger.debug "# fetion_uri = #{@fetion_uri}"
    @logger.debug "# http_tunnel = #{@http_tunnel}"
    @logger.debug "# fetionno = #{@fetionno}"
    @logger.debug "# mobileno = #{@mobileno}"
    @logger.debug "# password = #{@password.gsub(/./, '*')}"
  end

  def next_seq
    @seq += 1
  end

  def next_qseq
    @qseq += 1
  end

  def next_rseq
    @rseq += 1
  end

  def next_iseq
    @iseq += 1
  end

  def next_sseq
    @sseq += 1
  end

  def next_mseq
    @mseq += 1
  end

  def next_aseq
    @aseq += 1
  end

  def next_subseq
    @subseq += 1
  end

  def print text, crlf = "\n"
    STDOUT << text
    STDOUT << crlf if crlf
    STDOUT.flush
  end

  def compress data, expr = /\s*\n\s*/
    data.gsub(expr, '')
  end
end
